package br.mikhas.util.date;

import java.util.TimeZone;

/**
 * Immutable object which represents a date without regional data.
 * <p>
 * It is representated and easily parsed from ISO date format.
 * 
 * @author Mikhail Domanoski
 * 
 */
public final class Date implements Instant, Comparable<Date> {

	/**
	 * The time in millis
	 */
	private final long timestamp;

	/**
	 * The year field of the date
	 */
	private int year = -1;

	/**
	 * The month field of the date
	 */
	private int month = -1;

	/**
	 * The day field of the date
	 */
	private int day = -1;

	/**
	 * Cached string form of the string form of this date.
	 */
	private String stringed;

	/**
	 * The partial fields
	 */
	private int[] fields = new int[7];

	public Date() {
		long millis = System.currentTimeMillis();
		int offset = TimeZone.getDefault().getOffset(millis);

		this.denormalize(millis + offset);

		this.timestamp = DateCalculator.getYearMonthDayMillis(this.year,
				this.month, this.day);
	}

	/**
	 * Creates a new date defining the instant on it.
	 * 
	 * @param millis
	 *            The instant
	 */
	public Date(long millis) {
		this.denormalize(millis);
		this.timestamp = DateCalculator.getYearMonthDayMillis(this.year,
				this.month, this.day);
	}

	/**
	 * Creates a new date instance defining it's fields.
	 * 
	 * @param year
	 *            The year
	 * @param month
	 *            The month from 1 to 12 of the date.
	 * @param day
	 *            The day
	 */
	public Date(int year, int month, int day) {
		this.timestamp = DateCalculator.getYearMonthDayMillis(year, month, day);
		this.year = year;
		this.month = month;
		this.day = day;

		fields[Instant.YEAR] = this.year;
		fields[Instant.MONTH] = this.month;
		fields[Instant.DAY] = this.day;
	}

	/**
	 * Creates a new date from a ISO date string.
	 * <p>
	 * The string format must be like yyyy-MMdd.
	 * 
	 * @param date
	 *            A string representation of the date
	 */
	public Date(String date) {
		if (!this.validateISOFormat(date)) {
			throw new IllegalArgumentException("Incorrect date format: " + date);
		}

		this.year = Integer.parseInt(date.substring(0, 4));
		this.month = Integer.parseInt(date.substring(5, 7));
		this.day = Integer.parseInt(date.substring(8, 10));
		this.timestamp = DateCalculator.getYearMonthDayMillis(this.year,
				this.month, this.day);

		this.stringed = date;
	}

	/**
	 * Compares to Dates and return an integer.
	 * <p>
	 * It's comprared only the instant of the date.
	 */
	public int compareTo(Date other) {
		return (int) (this.timestamp - other.timestamp);
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		} else if (obj == this) {
			return false;
		} else {
			if (obj instanceof Date) {
				Date d = (Date) obj;
				return d.timestamp == this.timestamp;
			} else {
				return false;
			}
		}

	}

	@Override
	public int hashCode() {
		return (int) this.timestamp;
	}

	/**
	 * Represents this date using ISO date format.
	 * <p>
	 * Ex.: 1969-12-31
	 */
	@Override
	public String toString() {
		if (this.stringed == null) {

			if (year < 1969 || month < 0 || day < 1) {
				this.denormalize(this.timestamp);
			}

			StringBuilder builder = new StringBuilder(10);
			builder.append(year).append('-');

			if (month < 10) {
				builder.append('0');
			}
			builder.append(month).append('-');

			if (day < 10) {
				builder.append('0');
			}
			builder.append(day);

			stringed = builder.toString();
		}

		return stringed;
	}

	/**
	 * Checks if an string represents a date on ISO date format (yyyy-MM-dd).
	 * 
	 * @param date
	 *            A string-form date.
	 * @return <code>true</code> if the date matches the ISO fomat.
	 */
	private boolean validateISOFormat(String date) {
		if (date.length() == 10) {
			char c;

			// Check the format 0000-00-00
			for (int i = 0; i < 10; i++) {
				c = date.charAt(i);
				switch (i) {
				case 4:
				case 7:
					if (c != '-')
						return false;
					break;
				case 5:
					if (c < '0' || c > '1')
						return false;
					break;
				case 8:
					if (c < '0' || c > '3')
						return false;
					break;
				default:
					if (c < '0' || c > '9')
						return false;
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Returns the instant in milliseconds for this date
	 * 
	 * @return The instant milliseconds
	 */
	public long getTime() {
		return this.timestamp;
	}

	/**
	 * Extracts the components of the date from the instant.
	 * <p>
	 * It populates the <code>year</code>, <code>month</code> and
	 * <code>day</code> fields.
	 */
	private void denormalize(long millis) {
		this.year = DateCalculator.getYear(millis);
		this.month = DateCalculator.getMonth(millis, this.year);
		this.day = DateCalculator.getDay(millis, this.year, this.month);

		fields[Instant.YEAR] = this.year;
		fields[Instant.MONTH] = this.month;
		fields[Instant.DAY] = this.day;
	}

	@Override
	public boolean containsField(int field) {
		return field >= Instant.YEAR && field < Instant.HOUR;
	}

	@Override
	public int fieldCount() {
		return 3;
	}

	@Override
	public int getField(int field) {
		if (containsField(field))
			return fields[field];
		else
			throw new IllegalArgumentException("Date does not contains field #"
					+ field);
	}
}
